Migración AWS, mapa real, y fixes de KYC/CETES - #344
Open
ericmt-98 wants to merge 12 commits into
Open
Conversation
trustProxy: true trusted the leftmost X-Forwarded-For entry, which a client behind the ALB can spoof to evade per-IP rate limits. Pin to a single proxy hop instead. Node 20 is EOL; CI now builds with Node 22 to match the production container runtime.
Testing the ECS infra directly against RDS revealed pg-connection-string now treats sslmode=require as an alias for verify-full, so the app rejected RDS's cert as self-signed and looped through its 5 connect retries before exiting. The plan had this as a deferred hardening step (A5/Fase 9) but it's required now for the app to boot at all. Downloads the RDS global CA bundle at build time and chmods it readable by the non-root `node` user. DATABASE_URL in SSM updated separately to sslmode=verify-full&sslrootcert=/app/rds-global-bundle.pem. Verified against the real micopay-prod RDS instance via a standalone ECS task.
/merchants/available already filters by merchant_available=true, so every merchant returned is available by definition. The online field on Offer/OfferConfirmData and the agentStatus badge in TradeConfirmation were an invented signal (audit G2) — remove them instead of deriving a fake always-true value.
/merchants/available was public, unauthenticated, with no rate limit, and returned exact lat/lng — letting anyone scrape the full census of merchant locations. Add a 30 req/min per-IP rate limiter and round the publicly returned latitude/longitude to 3 decimals (~110m); distance_km still uses the exact stored coordinates via the existing SQL Haversine. Exact coordinates remain available to a counterparty only inside an accepted trade. Note: micopay/backend/package.json also carries pre-existing unrelated script additions (test:trade-auth, test:refund, test:challenge) from outside this change set — verified harmless/compatible, included because they share the same file/hunk as the new test:discovery script.
Replace the simulated PNG map (MapSim, bounding-box-normalized fake pins, user always centered, hardcoded "CDMX · ZONA CENTRO" / "Agentes reales cercanos") with MapReal: real tiles, real GPS-centered user position, real merchant coordinates, pan/zoom via MapLibre GL. - useMerchantsAvailable now exposes userPosition in its success state (previously resolved lat/lng then discarded them). - ExploreMap and DepositMap swapped to MapReal; MapSim marked @deprecated but kept (referenced elsewhere, removal is WP5). - VITE_MAP_STYLE_URL added to .env.testnet/.env.mainnet (empty — pending a MapTiler key from Eric); MapReal falls back to the public MapLibre demo style + a small "dev map" notice until it's set. - npm i maplibre-gl.
Backend already exposed PATCH /merchants/me/location, validated and authenticated, but the frontend never called it — so no real merchant could ever appear on the map (audit §3.3), only the 4 seed-demo ones. - api.ts: updateMerchantLocation() + MerchantLocation type; MerchantConfig now types the latitude/longitude/address_text fields the backend's GET /merchants/me/config already returns. - MapReal: new pickerMode/pickerPosition/onPickerPositionChange props for a single draggable pin, additive only — existing merchant/camera effect bails out early when pickerMode is set, non-picker behavior unchanged. - MerchantSettings: new "Mi ubicación" section — CTA using useGeolocation to get a GPS fix, MapReal picker to drag-adjust, optional address text, save via updateMerchantLocation. Location kept in separate state from `form` (PUT /merchants/me/config has additionalProperties:false, so merging would break the existing rate/limits save). - MerchantAvailabilityToggle: optional hasLocation prop drives a non-blocking warning when a merchant activates availability without a fixed location (soft gate per plan — does not block activation). - i18n: new merchantSettings.location.* keys (es/en).
…WP5) - Delete src/components/MapSim.tsx and public/map_bg.png (superseded by MapReal since WP1; no remaining consumers). - Delete micopay/backend/src/seed.ts (orphaned script, unreferenced by index.ts or package.json; the real seed is seedDemoMerchants() in index.ts, untouched). - docs/AUDIT_APK_MAPA_2026-07.md: mark G1/G2/G3/G6 and §3 (simulated render + missing location-capture pipeline) as resolved, referencing WP1-WP5 on this branch.
demotiles.maplibre.org only carries country-level geometry, so at the street zooms fitBounds produces in a town the map rendered as an empty background — first real-device test in Huatusco showed no map at all. OpenFreeMap's liberty style has full OSM street data, needs no API key, and permits production use. Compact attribution control added (OSM license requires visible credit); the "dev map" notice is gone since the fallback is no longer a dev-only style.
Real-device testing in Huatusco showed the map rendering nothing: no tiles, no roads, blank canvas, despite the network layer and WebGL context both working fine. Root cause, confirmed via remote DevTools attached to the WebView: maplibre-gl v6.0.0 (a fresh major with no patch releases yet) changed its internal tile-parsing worker to an ES module. Under Capacitor's https://localhost custom scheme, that worker's relative imports never resolve — no Worker target even showed up in DevTools, no thrown exception, but map.isStyleLoaded() stayed false forever and 'load'/ 'idle' never fired. A plain non-module Worker roundtrip worked fine in the same WebView, isolating the failure to v6's module-worker bundling specifically, not Workers in general. Fix: pin to maplibre-gl@5.24.0, the last major before the ESM-worker rewrite, which uses a classic importScripts worker with no such resolution issue. After downgrading, a Worker target appeared in DevTools and 'load'/'idle' fired normally; verified visually via a captured canvas screenshot with real street tiles. Also swaps the fallback style from demotiles.maplibre.org (country borders only — renders blank at street zoom, a second, independent gap found during the same session) to OpenFreeMap's `liberty` style (full OSM street data, no API key, production-safe). VITE_MAP_STYLE_URL is effectively no longer required — MapTiler handoff from the plan is now optional, not blocking.
…the cash-agent network
The "¿Sin cripto? Conecta tu banco vía SPEI" button on CETESScreen sent
users to /deposit — the P2P cash-agent discovery flow (DepositMap,
farmacia_guadalupe, etc.) — instead of the real Etherfuse onramp that's
already built into this same screen (payMethod === 'spei', the
getRampQuote('onramp', ...) path). Two completely different products;
this CTA promises a bank connection and delivered a cash meetup.
The SPEI payment method tab requires canDepositSpei (approved KYC), so
the fix is conditional: if the user already has approved KYC, the
click now reveals the in-page SPEI tab directly (setTab('buy') +
setPayMethod('spei')); otherwise it navigates to /kyc — the actual
prerequisite for connecting a bank — instead of the unrelated agent
flow. The CTA also hides itself once the SPEI tab is already showing,
since it'd otherwise sit there redundantly pointing at itself.
Etherfuse's POST /ramp/onboarding-url now rejects requests without
userInfo.email (their docs had flagged it "optional, will become
required in a future release" — that release landed in sandbox
2026-07-25). MicoPay's Stellar-keypair auth never collected an email
from anyone, so every /defi/kyc/start call was failing with a 502
wrapping "Json deserialize error: missing field `email`".
Backend: adds a nullable users.email column (migration), accepts an
optional email in the POST body, persists it once set, and returns a
new EMAIL_REQUIRED error if neither the column nor the request has one.
Frontend: KYCScreen prompts for an email inline when EMAIL_REQUIRED
comes back, then retries startKYC with it. Also fixes a second, adjacent
bug found while diagnosing this: handleOpenHostedFlow had no catch
block at all, so any failed startKYC() (this one included) silently
opened nothing and left the user staring at an unresponsive button —
this is what was actually reported ("no abre nada en el navegador").
extractApiErrorPayload only read response.data.error, but the backend's
error handler sends `code` (see index.ts setErrorHandler) — fixed so
EMAIL_REQUIRED and every other error code the backend already sends are
actually reachable from the frontend, not just the message string.
Verified by reproducing the exact 502 via curl against the real
sandbox and reading the underlying Etherfuse error from CloudWatch
logs before writing the fix.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resumen
Tres frentes de trabajo, en commits atómicos separados:
Migración a AWS (
1db550a,02232b4)trustProxy: 1(evita evasión de rate limits vía X-Forwarded-For) y Node 22 en CIverify-fullreal contra RDS con el CA bundle embebido en la imagen — descubierto necesario en vivo, no solo como endurecimiento diferidoMapa real (
f706c7f..1e7faf9, más 2 fixes defeat/map-realpost-merge)maplibre-gla v5 — la v6 rompe su Worker interno bajo el esquemahttps://localhostde Capacitor (verificado con DevTools remoto conectado al WebView)/merchants/available(privacidad — el endpoint era público sin límites)online: truehardcodeado) y limpia código muertoFixes de producto encontrados probando en dispositivo real
fix(cetes): el botón "¿Sin cripto?" mandaba a la red de agentes P2P en vez del ramp real de Etherfusefix(kyc): Etherfuse ahora exigeemail(antes opcional) y el flujo fallaba en silencio sin mostrar error — se agrega captura de email + manejo de errores realTest plan
npm run builden ambos)api.micopay.app): RDS conecta converify-full, migraciones corren, health check en verdePendiente (no bloqueante, documentado en docs/PLAN_MAPA_REAL_2026-07.md §7)
🤖 Generated with Claude Code